#include <stdio.h>
#include <stdlib.h>    // getenv
#include <assert.h>    // assert
#include <string.h>    // strlen, strtok
#include <unistd.h>    // fork, exec*, exit
#include <sys/types.h> // wait
#include <sys/wait.h>  // wait

// 定义成宏是为了方便改
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44

char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE]; // 用来保存当前路径
char myenv[LINE_SIZE]; // 保存自己导入的环境变量
int lastcode = 0; // 保存上一个命令的退出码
int quit = 0;

const char* get_username()
{
    return getenv("USER");
}

const char* get_hostname()
{
    return getenv("HOSTNAME");
}

const char* get_pwd()
{
    return getenv("PWD");
}

// 交互，互动，输入指令
void interact(char* cline, int size)
{ 
    // 命令行
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", get_username(), get_hostname(), get_pwd());
    char* s = fgets(cline, size, stdin); 
    assert(s); // assert编译时有效，运行时无效，debug模式有效，release模式无效
    (void)s;

    // "abcd\n\0"，strlen求出来的字符串长度是5，包括'\n'，'\n'的下标是4，所以要-1
    cline[strlen(cline) - 1] = '\0';
}

// 字符串分割
int splitstring(char cline[], char* _argv[])
{
    int i = 0;
    // 字符串分割，下面两行代码，就可以对整个字符串全部做切割，并且将argv最后一个参数置为了NULL
    _argv[i++] = strtok(cline, DELIM);
    while(_argv[i++] = strtok(NULL, DELIM));

    // 接收到NULL之后，i又++了一次，所以此时参数个数是i-1
    return i - 1;
}

// 创建子进程执行命令
void NormalExcute(char* _argv[])
{

    pid_t id = fork();    
    if (id < 0)    
    {
        perror("fork");    
        return;
    }    
    else if (id == 0)    
    {    
        // 让子进程执行命令    
        //execvpe(_argv[0], _argv, environ);    
        execvp(_argv[0], _argv);
        exit(EXIT_CODE); // 只有替换失败，才会走到这里    
    }    
    else    
    {    
        // 让父进程等待子进程    
        int status = 0;    
        pid_t rid = waitpid(id, &status, 0);    
        if (rid == id)    
        {   
            // 获取退出码
            lastcode = WEXITSTATUS(status);  
        }    
    }
}

// 内键命令
int inner_key_command(int _argc, char* _argv[])
{
    if (_argc == 2 && strcmp(_argv[0], "cd") == 0)
    {
        chdir(_argv[1]); // 更改当前路径
        getcwd(pwd, LINE_SIZE); // 获取当前路径，存到字符数组pwd中
        sprintf(getenv("PWD"), "%s", pwd); // 把pwd里的字符串刷新到环境变量PWD中
        lastcode = 0;
        return 1;
    }
    
    if (_argc == 2 && strcmp(_argv[0], "export") == 0)
    {
        strcpy(myenv, _argv[1]);
        putenv(myenv);
        lastcode = 0;
        return 1;
    }

    if (_argc == 2 && strcmp(_argv[0], "echo") == 0)
    {
        if (strcmp(_argv[1], "$?") == 0)
        {
            printf("%d\n", lastcode);
        }
        else if (*_argv[1] == '$')
        {
            // getenv，如果有就返回环境变量的值，如果没有就返回NULL
            char* val = getenv(_argv[1] + 1);
            if (val) printf("%s\n", val);
        }
        else 
        {
            // 这里还要处理双引号等，就不处理了，直接打印吧
            printf("%s\n", _argv[1]);
        }
        lastcode = 0;
        return 1;
    }

    // 特殊处理：ls带颜色
    if (strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }

    return 0;
}

int main()
{
    while (!quit)
    {
        // 1. 
        // 2. 交互问题，获取命令行
        interact(commandline, sizeof(commandline));

        // 假设：commandline -> "ls -a -l -n\0"，拿到指令后，接下来就是执行命令，先做字符串分割
        // 3. 子串分割，解析命令行
        int argc = splitstring(commandline, argv);
        if (argc == 0) continue;

        // 4. 指令的判断，指令分内键命令和普通命令
        int n = inner_key_command(argc, argv);

        // 5. 普通命令的执行--交给子进程
        if (!n) NormalExcute(argv);
    }
    return 0;
}



